
本篇介紹 ES2018 (ES9) 提供的 Promise.prototype.finally()。
下面是幾個非同步處理很常見的情境:
以上情境都有一個共通點:不管做什麼事,最後都要做某件事。
也許你會想到 try-finally,會希望非同步處理的 Promise 上也有 finally 的功能 (我自己是沒想過啦 XD),這就是今天要介紹的 Promise.prototype.finally()!
在過去原生的 Promise 沒有提供 finally 功能時,很多 library 都在非同步處理的 API 上實作了 finally() 方法,此方法是用來註冊一個在 promise settled 時 (即 fulfilled 或 rejected) invoke 用的 callback。
更多 library 的實作可參閱:
在 ES2018 (ES9) 提供了 Promise.prototype.finally() 新的 Promise method。當 promise settled 時 (即 fulfilled 或 rejected),會執行指定的 callback。
先說明什麼是 promise chain,因為之後會常常看到這個專有名詞。
將多個 Promise 串在一起,以表達一個序列的非同步執行步驟,而這個序列就是 promise chain。
那為何是 chain?因為每次在 Promise 上呼叫 .then()、.catch()、.finally() 等 Promise method 時,都會建立並回傳新的 Promise。例如:
Promise.resolve('OK')
.then(result => {
console.log(result);
return Promise.resolve('Hi')
})
.then(result => {
console.log(result);
return Promise.reject('Oops');
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log('finally');
});
// OK
// Hi
// Oops
// finally
Promise.prototype.finally() 的回傳值永遠是 PromisePromise.prototype.finally() 的回傳值永遠是 Promise 物件,該 promise 可能會 fulfilled 或 rejected,那何時會 fulfilled?還是會 rejected?
先講結論:要看你的 promise chain 是怎麼寫的
.finally() 的前一個 Promise 是 fulfilled,那 .finally() 回傳的 Promise 就會是 fulfilled.finally() 的前一個 Promise 是 rejected,那 .finally() 回傳的 Promise 就會是 rejected先看幾個範例:
假設我先執行 Promise.resolve('OK'),該 promise 會立即 fulfilled,將 OK 傳給 .then() 的 callback,所以第一個輸出訊息會是 OK,接著執行 .finally(),並將 .finally() 的回傳值存在一個名為 promiseA 的變數:
let promiseA = Promise.resolve('OK')
.then(result => {
console.log(result);
})
.finally(() => {
console.log('finally');
});
// OK
// finally
接著印出 promiseA,它是一個 Promise 物件,該 promise 已經 fulfilled 了,且 fulfilled 的值為 undefined:
console.log(promiseA);
// Promise {<fulfilled>: undefined}
那為何 fulfilled 的值會是 undefined,因為在 promise chain 中,.finally() 的前一個 Promise 是 .then() 回傳的,而 .then() 的 callback 沒有回傳值,所以才會是 undefined。
所以不要搞錯了,
promiseA存的不是Promise.resolve('OK')回傳的Promise物件,而是最後一個 promise chain 的。
那再看下一個範例,這次拿到 .then() 這個步驟,一樣將的回傳值存起來,存在一個名為 promiseB 的變數:
let promiseB = Promise.resolve('OK')
.finally(() => {
console.log('finally');
});
// finally
接著印出 promiseB,該 promise 一樣已經 fulfilled 了,但這次 fulfilled 的值是 OK:
console.log(promiseB);
// Promise {<fulfilled>: "OK"}
為什麼會是 OK?因為在 promise chain 中,.finally() 的前一個 Promise 是 Promise.resolve('OK') 回傳的,該 Promise fulfilled 的值就是 OK,所以才會是 OK。
所以就如同前面結論說的,.finally() 回傳的 Promise 是 fulfilled 還是 rejected,是依據 promise chain 中前一個 Promise 來決定的。
Promise.prototype.finally() 的 callbackPromise.prototype.finally() 的 callback 沒有 argument.then() 和 .catch() 的 callback 會有 argument,而該 argumemt 是在 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。
但 Promise.prototype.finally() 的 callback 是沒有 argument 的,若你還是寫了 argument,其值也會是 undefined,不管 promise chain 中的前一個 Promise 的 fulfilled 或 rejected:
Promise.resolve('OK')
.finally(value => {
console.log(value);
});
// undefined
Promise.reject('Oops')
.finally(value => {
console.log(value);
});
// undefined
Promise.prototype.finally() 的 callback 會被忽略 returnPromise.prototype.finally() 的 callback 中的 return 會被忽略,但回傳的 Promise 的 fulfilled 值或 rejected 值會是 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。:
例如:Promise.resolve('OK') 會立即 fulfilled,接著在 .finally() 內 return 會被忽略,但 .finally() 回傳的 Promise 的 fulfilled 值會跟 Promise.resolve('OK') 回傳的 fulfilled 值相同。
Promise.resolve('OK')
.finally(() => {
console.log('finally...');
return 'finally';
})
.then(value => {
console.log(value);
});
若拆開 promise chain 就會更容易看出來:
let promiseA = Promise.resolve('OK');
console.log(promiseA);
// Promise {<fulfilled>: "OK"}
let promiseB = promiseA.finally(() => {
console.log('finally...');
return 'finally';
});
// finally...
console.log(promiseB);
// Promise {<fulfilled>: "OK"}
let promiseC = promiseB.then(value => {
console.log(value);
});
// OK
console.log(promiseC);
// Promise {<fulfilled>: "undefined"}
promise rejected 的情況也一樣,你可以試著將上面的 Promise.resolve('OK') 改成 Promise.reject('Oops') 觀察看看。
Promise.prototype.finally() vs. finally clause先來看兩者的寫法。
下面是 Promise.prototype.finally() 的用法:
Promise.resolve('OK')
.then(result => {
console.log(result);
})
.catch(error => {
console.log('error');
})
.finally(() => {
console.log('finally');
});
// OK
// finally
而下面是 try 陳述句中 finally clause 的用法:
try {
console.log('OK');
} catch (error) {
console.log('error');
} finally {
console.log('finally');
}
// OK
// finally
兩者有些地方很相識,但用法和行為都不同,下面會提出它們的不同之處。
return 值Promise.prototype.finally() 會回傳 Promise,該 Promise 可能會 fulfilled 或 rejected (前面有說明)。
而 finally 只是 try 陳述句中的 clause,若在 finally clause 內 return 某個值會成為 function 的回傳值。
例如:在 func() 函數中,finally clause 內 return 的 func 就成為此函數的回傳值:
function func() {
try {
console.log('try');
} catch (error) {
console.log('catch');
} finally {
console.log('finally');
return 'func';
}
}
let result = func();
// try
// finally
console.log(result);
// "func"
throw 值若在 finally clause 內使用 throw,需要讓另一個 try-catch 來捕捉錯誤:
function func() {
try {
console.log('try');
} finally {
console.log('finally');
throw new Error('Oops');
}
}
try {
func();
} catch(error) {
console.log(error);
}
// try
// finally
// Error: Oops
// at func (<anonymous>:6:11)
// at <anonymous>:11:3
而在 Promise.prototype.finally() 的 callback 中使用 throw,會讓回傳的 Promise rejected:
let promiseA = Promise.resolve('OK')
.then(result => {
console.log(result);
});
// OK
console.log(promiseA);
// Promise {<fulfilled>: undefined}
let promiseB = promiseA.finally(() => {
console.log('finally');
throw new Error('Oops');
});
// finally
// Uncaught (in promise) Error: Oops
// at <anonymous>:3:11
// at <anonymous>
console.log(promiseB);
// Promise {<rejected>: Error: Oops
// at <anonymous>:3:11
// at <anonymous>}
Promise.prototype.finally() 和 finally clause 的其中一個共通點就是一定會執行。
finally clause 一定會在最後執行先來說明 finally clause。
在函數內的 try clause 或 catch clause 裡面 return 某個值,函數會在回傳該值之前,先執行 finally clause 內的程式碼 (所以 finally 就如其名,真的是「最後」)。
例如:在 try clause 內 return 值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally clause 內的程式碼:
function func() {
try {
console.log('try');
return 'func';
} catch (error) {
console.log('catch');
} finally {
console.log('finally');
}
}
console.log(func());
// try
// finally
// "func"
另一個範例:在 catch clause 內 return 值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally clause 內的程式碼:
function func() {
try {
console.log(data);
} catch (error) {
console.log('catch');
return 'func';
} finally {
console.log('finally');
}
}
console.log(func());
// catch
// finally
// "func"
Promise.prototype.finally() 的 callback 都會執行不管 Promise 是 fulfilled 或 rejected 都會執行 Promise.prototype.finally() 內的 callback。
例如:Promise.resolve('OK') 會回傳的 promise 立即 fulfilled 後,會執行 .finally() 的 callback:
let promiseA = Promise.resolve('OK')
.finally(() => {
console.log('finally');
});
// finally
console.log(promiseA);
// Promise {<fulfilled>: "OK"}
另一個例子:Promise.reject('Oops') 會回傳的 promise 立即 rejected 後,會執行 .finally() 的 callback:
let promiseB = Promise.reject('Oops')
.finally(() => {
console.log('finally');
});
console.log(promiseB);
// finally
前面提到一些情境,就拿其中一個作為範例。
假設進入某頁面時,會立即發 AJAX request,在拿到 response 之前都會顯示「正在載入...」的訊息,不管是拿到 response,還是發生錯誤,都會隱藏「正在載入...」。
範例程式碼如下:
let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';
function fetchData(url) {
return fetch(url)
.then(response => {
console.log('isLoading:', isLoading);
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return response.json();
} else {
throw new TypeError(`Oops, we haven't got JSON!`);
}
})
.then(json => {
console.log('Success');
return json;
})
.catch(error => {
console.log(error);
})
.finally(() => {
isLoading = false;
console.log('isLoading:', isLoading);
});
}
// 試著換成 fetchData(HTML_API)
fetchData(JSON_API).then(data => {
console.log(data);
});
之後會提到
?.Optional Chaining 運算子。
若 API 的 Content-Type 是 application/json (即 fetch(JSON_API) 這個 AJAX response),promise 就會 fulfilled 列印出 API 資料,並且在 finally 時將 isLoading 設為 false,所以輸出如下:
fetchData(JSON_API).then(data => {
console.log(data);
});
// isLoading: true
// Success
// isLoading: false
// {userId: 1, id: 1, title: "..."}
若 API 的 Content-Type 不是 application/json (即 fetch(HTML_API) 這個 AJAX response),promise 就會 rejected 列印出錯誤訊息,並且在 finally 時將 isLoading 設為 false,所以輸出如下:
fetchData(HTML_API).then(data => {
console.log(data);
});
// isLoading: true
// TypeError: Oops, we haven't got JSON!
// isLoading: false
// undefined
因為不管是 .then() 或 .catch() 都要執行 isLoading = false,那更好的作法就是統一在 .finally() 執行 isLoading = false,這樣就不用寫重複的邏輯了。
若上面的範例改用 async / await 的寫法也許會像這樣:
let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';
async function fetchData(url) {
try {
const response = await fetch(url);
console.log('isLoading:', isLoading);
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
console.log('Success');
return response.json();
} else {
throw new TypeError(`Oops, we haven't got JSON!`);
}
} catch(error) {
console.log(error);
} finally {
isLoading = false;
console.log('isLoading:', isLoading);
}
}